package com.example.origin.advice.config;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.example.origin.advice.annotation.WrapResult;
import com.example.origin.advice.constants.CommonConstants;
import com.example.origin.advice.constants.ErrorConstants;
import com.example.origin.advice.entity.ErrorResult;
import com.example.origin.advice.entity.ResponseResult;
import com.example.origin.advice.error.BusinessException;
import com.example.origin.advice.error.ServerException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.util.NestedServletException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author yutao.zhang
 * @create 2023-07-25 16:38
 **/
@Slf4j
@RestControllerAdvice(basePackages = CommonConstants.BASE_PACKAGES)
public class GlobalRestControllerAdvice {

    //应用到所有@RequestMapping注解的方法，在其抛出Exception异常时执行
    //定义全局异常处理，value属性可以过滤拦截指定异常，此处拦截所有的Exception
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.OK)
    public ResponseResult<Void> handleException(Throwable e) throws Throwable {

        for (StackTraceElement stackTraceElement : e.getStackTrace()) {
            String className = stackTraceElement.getClassName();
            if (className.startsWith(CommonConstants.BASE_PACKAGES)) {
                String methodName = stackTraceElement.getMethodName();
                try {
                    Method[] methods = ReflectUtil.getMethods(Class.forName(className));
                    for (Method method : methods) {
                        if (method.getName().equals(methodName)) {
                            WrapResult annotation = method.getAnnotation(WrapResult.class);
                            if (annotation != null && !annotation.value() && isSameMethod(method)) {
                                throw e;
                            }
                        }
                    }
                } catch (ClassNotFoundException classNotFoundException) {
                    log.error(classNotFoundException.getMessage(), classNotFoundException);
                }
            }
        }
        return resolveError(e);
    }

    private boolean isSameMethod(Method method) {
        ServletRequestAttributes servletRequestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (servletRequestAttributes != null) {
            HandlerMethod handlerMethod = (HandlerMethod) servletRequestAttributes.getRequest().getAttribute("org.springframework.web.servlet.HandlerMapping.bestMatchingHandler");
            if (!method.getName().equals(handlerMethod.getMethod().getName())) {
                return false;
            }
            if (method.getParameterCount() != handlerMethod.getMethod().getParameterCount()) {
                return false;
            }
            Class<?>[] methodParameterTypes = method.getParameterTypes();
            Class<?>[] parameterTypes = handlerMethod.getMethod().getParameterTypes();
            for (int i = 0; i < methodParameterTypes.length; i++) {
                if (methodParameterTypes[i] != parameterTypes[i]) {
                    return false;
                }
            }
        }
        return true;
    }


    private ResponseResult<Void> resolveError(Throwable ex) {
        log.error(ex.getMessage(), ex);
        ResponseResult<Void> result = new ResponseResult<>();
        result.setSuccess(false);
        try {
            if (ex instanceof BusinessException) {
                for (ErrorResult error : ((BusinessException) ex).getErrors()) {
                    result.addError(error);
                }
            } else if (ex instanceof ServerException) {
                for (ErrorResult error : ((ServerException) ex).getErrors()) {
                    result.addError(error);
                }
            } else if (ex instanceof ConstraintViolationException) {
                Set<ConstraintViolation<?>> violationSet = ((ConstraintViolationException) ex).getConstraintViolations();
                if (!ObjectUtils.isEmpty(violationSet)) {
                    violationSet.forEach(v -> {
                        String name = subAfter(v.getConstraintDescriptor().getAnnotation().annotationType().getName(), ".");
                        result.addError(new ErrorResult(humpToLine(name), v.getMessage()));
                    });
                }
            } else if (ex instanceof MethodArgumentNotValidException) {
                BindingResult bindingResult = ((MethodArgumentNotValidException) ex).getBindingResult();
                List<FieldError> errors = bindingResult.getFieldErrors();
                if (!ObjectUtils.isEmpty(errors)) {
                    List<ErrorResult> errorInfoList = errors.stream().map(err -> {
                        String code = Objects.requireNonNull(err.getCode());
                        return new ErrorResult(humpToLine(code), err.getDefaultMessage());
                    }).collect(Collectors.toList());
                    result.setErrors(errorInfoList);
                }
            } else if (ex instanceof BindException) {
                BindingResult bindingResult = ((BindException) ex).getBindingResult();
                List<FieldError> fieldErrors = bindingResult.getFieldErrors();
                if (!ObjectUtils.isEmpty(fieldErrors)) {
                    fieldErrors.forEach(fieldError -> {
                        result.addError(new ErrorResult(ErrorConstants.SERVER_ERROR, fieldError.getDefaultMessage()));
                    });
                }
            } else if (ex instanceof NestedServletException) {
                Throwable cause = ex.getCause();
                return resolveError(cause);
            } else {
                //这里是未知异常，直接报服务器错误
                result.addError(new ErrorResult(ErrorConstants.SERVER_ERROR, ErrorConstants.SERVER_ERROR));
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            //此处已经获取异常信息失败了，直接返回错误消息
            result.addError(new ErrorResult(ErrorConstants.SERVER_ERROR, ErrorConstants.SERVER_ERROR));
        }
        return result;
    }


    private static String subAfter(String str, String separator) {
        return StrUtil.subAfter(str, separator, true);
    }


    private static String humpToLine(String str) {
        String lowerCase = str.replaceAll("[A-Z]", "_$0").toLowerCase();
        return lowerCase.substring(1);
    }


}
